package com.hqd.ch03.v51.boot.context.properties.bind.handler;

import com.hqd.ch03.v51.boot.context.properties.bind.*;
import com.hqd.ch03.v51.boot.context.properties.source.ConfigurationProperty;
import com.hqd.ch03.v51.boot.context.properties.source.ConfigurationPropertyName;
import com.hqd.ch03.v51.boot.context.properties.source.ConfigurationPropertySource;
import com.hqd.ch03.v51.boot.context.properties.source.IterableConfigurationPropertySource;

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;

public class NoUnboundElementsBindHandler extends AbstractBindHandler {

    private final Set<ConfigurationPropertyName> boundNames = new HashSet<>();

    private final Set<ConfigurationPropertyName> attemptedNames = new HashSet<>();

    private final Function<ConfigurationPropertySource, Boolean> filter;

    NoUnboundElementsBindHandler() {
        this(BindHandler.DEFAULT, (configurationPropertySource) -> true);
    }

    public NoUnboundElementsBindHandler(BindHandler parent) {
        this(parent, (configurationPropertySource) -> true);
    }

    public NoUnboundElementsBindHandler(BindHandler parent, Function<ConfigurationPropertySource, Boolean> filter) {
        super(parent);
        this.filter = filter;
    }

    @Override
    public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
        this.attemptedNames.add(name);
        return super.onStart(name, target, context);
    }

    @Override
    public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
        this.boundNames.add(name);
        return super.onSuccess(name, target, context, result);
    }

    @Override
    public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Exception error)
            throws Exception {
        if (error instanceof UnboundConfigurationPropertiesException) {
            throw error;
        }
        return super.onFailure(name, target, context, error);
    }

    @Override
    public void onFinish(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result)
            throws Exception {
        if (context.getDepth() == 0) {
            checkNoUnboundElements(name, context);
        }
    }

    private void checkNoUnboundElements(ConfigurationPropertyName name, BindContext context) {
        Set<ConfigurationProperty> unbound = new TreeSet<>();
        for (ConfigurationPropertySource source : context.getSources()) {
            if (source instanceof IterableConfigurationPropertySource && this.filter.apply(source)) {
                collectUnbound(name, unbound, (IterableConfigurationPropertySource) source);
            }
        }
        if (!unbound.isEmpty()) {
            throw new UnboundConfigurationPropertiesException(unbound);
        }
    }

    private void collectUnbound(ConfigurationPropertyName name, Set<ConfigurationProperty> unbound,
                                IterableConfigurationPropertySource source) {
        IterableConfigurationPropertySource filtered = source.filter((candidate) -> isUnbound(name, candidate));
        for (ConfigurationPropertyName unboundName : filtered) {
            try {
                unbound.add(
                        source.filter((candidate) -> isUnbound(name, candidate)).getConfigurationProperty(unboundName));
            } catch (Exception ex) {
            }
        }
    }

    private boolean isUnbound(ConfigurationPropertyName name, ConfigurationPropertyName candidate) {
        if (name.isAncestorOf(candidate)) {
            return !this.boundNames.contains(candidate) && !isOverriddenCollectionElement(candidate);
        }
        return false;
    }

    private boolean isOverriddenCollectionElement(ConfigurationPropertyName candidate) {
        int lastIndex = candidate.getNumberOfElements() - 1;
        if (candidate.isLastElementIndexed()) {
            ConfigurationPropertyName propertyName = candidate.chop(lastIndex);
            return this.boundNames.contains(propertyName);
        }
        Indexed indexed = getIndexed(candidate);
        if (indexed != null) {
            String zeroethProperty = indexed.getName() + "[0]";
            if (this.boundNames.contains(ConfigurationPropertyName.of(zeroethProperty))) {
                String nestedZeroethProperty = zeroethProperty + "." + indexed.getNestedPropertyName();
                return isCandidateValidPropertyName(nestedZeroethProperty);
            }
        }
        return false;
    }

    private boolean isCandidateValidPropertyName(String nestedZeroethProperty) {
        return this.attemptedNames.contains(ConfigurationPropertyName.of(nestedZeroethProperty));
    }

    private Indexed getIndexed(ConfigurationPropertyName candidate) {
        for (int i = 0; i < candidate.getNumberOfElements(); i++) {
            if (candidate.isNumericIndex(i)) {
                return new Indexed(candidate.chop(i).toString(),
                        candidate.getElement(i + 1, ConfigurationPropertyName.Form.UNIFORM));
            }
        }
        return null;
    }

    private static final class Indexed {

        private final String name;

        private final String nestedPropertyName;

        private Indexed(String name, String nestedPropertyName) {
            this.name = name;
            this.nestedPropertyName = nestedPropertyName;
        }

        String getName() {
            return this.name;
        }

        String getNestedPropertyName() {
            return this.nestedPropertyName;
        }

    }

}

